﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using RbmlVisualizer.Data.Blocks;
using RbmlVisualizer.Data;
using RbmlVisualizer.Logging;
using RbmlVisualizer.Scoring; 

namespace RbmlVisualizer.DiagramComparer
{
    public class DiagramComparer
    {
        // RBML blocks and all the UML blocks that match that RBML
        private readonly Dictionary<ClassifierBlock, List<ClassifierBlock>> _rbmlBlockToUmlBlockMapping = new Dictionary<ClassifierBlock, List<ClassifierBlock>>();
        private readonly List<Tuple<string, string>> _mappingsToCheck = new List<Tuple<string, string>>();
        
        private ScoringEngine myScorer = new ScoringEngine(); 

        // Score from scorer engine 
        private double _score;
        public double Score
        {
            get { return _score; }
        } 

        // Main function that is called outside of the bool 
        
        private void AddMappingToCheck(string rbml, string uml)
        {
            _mappingsToCheck.Add(new Tuple<string, string>(rbml, uml));
        }

        public bool CheckConformance(IList<ClassifierBlock> umlBlocks, IList<ClassifierBlock> rbmlBlocks)
        {
            myScorer.RegisterRbmlBlocks(rbmlBlocks);
            myScorer.RegisterUmlBlocks(umlBlocks);

            var conforming = true;

            foreach (var rbmlBlock in rbmlBlocks)
            {
                // Checks local conformance of blocks 
                var conformingUmlBlocks = GetConformingBlocks(rbmlBlock, umlBlocks);
                if (conformingUmlBlocks.Count == 0)
                    BlockLoggingService.Instance.LocalMatchNotFound(rbmlBlock);

                _rbmlBlockToUmlBlockMapping.Add(rbmlBlock, conformingUmlBlocks);
            }

            RemoveTypeMappingErrors(_rbmlBlockToUmlBlockMapping, _mappingsToCheck.Distinct());
            //we no longer need the values in this.
            _mappingsToCheck.Clear();

            foreach (var keyValuePair in _rbmlBlockToUmlBlockMapping.Where(kvp => kvp.Value.Count == 0))
            {
                BlockLoggingService.Instance.TypeMappingIvalidatedBlocks(keyValuePair.Key);
                conforming = false;
            }

            myScorer.ScoreLocalyConformingBlocks(_rbmlBlockToUmlBlockMapping);

            // Run the global conformity algorithm 
            if (!CheckGlobalMappings())
                conforming = false;

            //Check global multiplicity
            foreach (var rbmlBlock in _rbmlBlockToUmlBlockMapping.Keys)
            {
                var count = _rbmlBlockToUmlBlockMapping[rbmlBlock].Count;
                if (!CheckGlobalMultiplicity(rbmlBlock,count ))
                {
                    BlockLoggingService.Instance.GlobalMultiplicityFail(rbmlBlock, count);
                    conforming = false;
                }
            }

            myScorer.ScoreGloballyConformingBlocks(_rbmlBlockToUmlBlockMapping);
            this._score = myScorer.Score; 

            return conforming;
        }

        private static void RemoveTypeMappingErrors(Dictionary<ClassifierBlock, List<ClassifierBlock>> rbmlBlockToUmlBlockMapping, IEnumerable<Tuple<string, string>> potentiallyBadMappings)
        {
            bool foundBadMapping;
            do
            {
                //reset the foundBadMapping
                foundBadMapping = false;
                foreach (var rbmlUmlTuple in potentiallyBadMappings)
                {
                    //TODO could use a method that takes functions to use in the linq and returns list of bad mappings
                    Tuple<string, string> tuple = rbmlUmlTuple;
                    //For all the rbmlKeys that have the type in the potentially bad list lining up to classifier1
                    foreach (var rbmlKey in rbmlBlockToUmlBlockMapping.Keys.Where(c => c.Classifier1.Name == tuple.Item1))
                    {
                        var mappingsToRemove = new List<ClassifierBlock>();
                        var umlBlocks = rbmlBlockToUmlBlockMapping[rbmlKey];
                        foreach (var umlBlock in umlBlocks.Where(c => c.Classifier1.Name == tuple.Item2))
                        {
                            if (rbmlKey.Classifier1.Name != rbmlUmlTuple.Item1 || umlBlock.Classifier1.Name != rbmlUmlTuple.Item2)
                            {
                                BlockLoggingService.Instance.TypeMappingRemovedBlock(tuple, rbmlKey, umlBlock);
                                mappingsToRemove.Add(umlBlock);
                                foundBadMapping = true;
                            }
                        }
                        mappingsToRemove.ForEach(b => umlBlocks.Remove(b));
                    }

                    //For all the rbmlKeys that have the type in the potentially bad list lining up to classifier2
                    foreach (var rbmlKey in rbmlBlockToUmlBlockMapping.Keys.Where(c => c.Classifier2.Name == tuple.Item1))
                    {
                        var mappingsToRemove = new List<ClassifierBlock>();
                        var umlBlocks = rbmlBlockToUmlBlockMapping[rbmlKey];
                        foreach (var umlBlock in umlBlocks.Where(c => c.Classifier2.Name == tuple.Item2))
                        {
                            if (rbmlKey.Classifier2.Name != rbmlUmlTuple.Item1 || umlBlock.Classifier2.Name != rbmlUmlTuple.Item2)
                            {
                                BlockLoggingService.Instance.TypeMappingRemovedBlock(tuple, rbmlKey, umlBlock);
                                mappingsToRemove.Add(umlBlock);
                                foundBadMapping = true;
                            }
                        }

                        mappingsToRemove.ForEach(b => umlBlocks.Remove(b));
                    }
                }
            } while (foundBadMapping);
        }

        // Remove UML to RBML mappings that do not conform 
        private bool CheckGlobalMappings()
        {
            var conforming = true;
            foreach (var rbmlKey in _rbmlBlockToUmlBlockMapping.Keys)
            {
                // Get all overlapping RBML Blocks 
                var rbmlBlockList = GetOverlappingBlocks(rbmlKey, _rbmlBlockToUmlBlockMapping.Keys);
                // Pass those RBML blocks and the current RBML block to the function to remove the UML that don't work
                var nonConformingUmls = CheckAllRbmLtoUmlMappings(rbmlKey, rbmlBlockList);
                if (nonConformingUmls.Count > 0)
                {
                    foreach (var nonConformingUml in nonConformingUmls)
                    {
                        _rbmlBlockToUmlBlockMapping[rbmlKey].Remove(nonConformingUml);
                        LoggingService.Instance.WriteDebug("{0} was removed from {1}'s mapping.", nonConformingUml, rbmlKey);
                    }
                }

                if (_rbmlBlockToUmlBlockMapping[rbmlKey].Count == 0)
                {
                    BlockLoggingService.Instance.GlobalConformityNoMatches(rbmlKey);
                    conforming = false;
                }

            }

            return conforming;
        }

        // Check the RBML to UML mapping
        private List<ClassifierBlock> CheckAllRbmLtoUmlMappings(ClassifierBlock entry, List<ClassifierBlock> overlappingRbmlBlockList)
        {
            var allUmlBlocks = new List<ClassifierBlock>();
            foreach (var umlList in _rbmlBlockToUmlBlockMapping.Values)
            {
                allUmlBlocks.AddRange(umlList);
            }
            var distinctUmlBlocks = allUmlBlocks.Distinct();

            var umlBlocksToRemove = new List<ClassifierBlock>();

            // For each possible mapping block for the current block, get all overlapping RBML blocks
            foreach (var currentUmlBLock in _rbmlBlockToUmlBlockMapping[entry])
            {
                // For that possible mapping, get all of the overlapping UML blocks 
                List<ClassifierBlock> umlOverlappingBlockList = GetOverlappingBlocks(currentUmlBLock, distinctUmlBlocks);

                // Now for all the overlapping UML blocks, see if there is a corresponding RBML block for it
                if (!CheckIfRbmlBlocksOverlap(overlappingRbmlBlockList, umlOverlappingBlockList))
                {
                    // Remove the possible RBML mapping from the UML to RBML block
                    umlBlocksToRemove.Add(currentUmlBLock);
                }
            }
            return umlBlocksToRemove;
        }

        // Another function to check for us if that possible mapping can work 
        private bool CheckIfRbmlBlocksOverlap(List<ClassifierBlock> rbmlBlockList, List<ClassifierBlock> blockListTwo)
        {
            // Go through each uml block and see if it is mapped to a valid rbml block 
            // Can add something in here about multiplication 
            // For example, if its possible for classifier to have a cardinaltity of 0, don't even check for a possible mapping 
            foreach (var currentBlock in rbmlBlockList)
            {
                if (!CheckIfRbmlMapsToOneInList(currentBlock, blockListTwo))
                    return false;
            }
            return true;
        }

        // If the block is mapped to a "valid" block, accept 
        private bool CheckIfRbmlMapsToOneInList(ClassifierBlock rbmlBlock, IEnumerable<ClassifierBlock> blockList)
        {
            foreach (var currentBlock in blockList)
            {
                if (_rbmlBlockToUmlBlockMapping[rbmlBlock].Contains(currentBlock))
                    return true;
            }
            return false;
        }


        // Function to find all of the Blocks that overlap with currentBlock
        private static List<ClassifierBlock> GetOverlappingBlocks(ClassifierBlock currentBlock, IEnumerable<ClassifierBlock> blockList)
        {
            // All overlapping blocks
            List<ClassifierBlock> overlappingBlocks = new List<ClassifierBlock>();

            foreach (var checkForClassifier in blockList)
            {
                //We don't want the block to match itself
                if (currentBlock.Equals(checkForClassifier))
                    continue;

                // Get all the blocks that match, but not the block itself 
                if (currentBlock.Classifier1.Equals(checkForClassifier.Classifier1) || currentBlock.Classifier1.Equals(checkForClassifier.Classifier2))
                {
                    overlappingBlocks.Add(checkForClassifier);
                    LoggingService.Instance.WriteDebug("UML block {0} overlaps with UML block {1}", currentBlock.ToString(), checkForClassifier.ToString());

                }
                else if (currentBlock.Classifier2.Equals(checkForClassifier.Classifier1) || currentBlock.Classifier2.Equals(checkForClassifier.Classifier2))
                {
                    overlappingBlocks.Add(checkForClassifier);
                    LoggingService.Instance.WriteDebug("UML block {0} overlaps with UML block {1}", currentBlock.ToString(), checkForClassifier.ToString());
                }
            }
            return overlappingBlocks;
        }

        // Check local conformance
        public List<ClassifierBlock> GetConformingBlocks(ClassifierBlock roleBlock, IList<ClassifierBlock> umlBlocks)
        {
            var sameBlockTypeUmlBlocks = umlBlocks.Where(u => u.Relationship.BlockType == roleBlock.Relationship.BlockType && LineUpClassifiersForBlocks(roleBlock.Relationship, u));
            switch (roleBlock.Relationship.BlockType)
            {
                case BlockType.Generalization:
                case BlockType.Dependency:
                    return GetConformingDependencyOrGeneralizationBlocks(roleBlock, sameBlockTypeUmlBlocks);
                case BlockType.Association:
                    return GetConformingAssociationBlocks(roleBlock, sameBlockTypeUmlBlocks);
                default:
                    throw new InvalidOperationException(string.Format("A blocktype of {0} cannot be checked.", roleBlock.Relationship.BlockType));
            }
        }

        private List<ClassifierBlock> GetConformingDependencyOrGeneralizationBlocks(ClassifierBlock roleBlock, IEnumerable<ClassifierBlock> umlBlocks)
        {
            var rbmlBlocksAnswer = new List<ClassifierBlock>();
            foreach (var umlBlock in umlBlocks)
            {
                BlockLoggingService.Instance.LocalConformityStarting(roleBlock, umlBlock);

                if (CheckClassifier(roleBlock.Classifier1, umlBlock.Classifier1) && CheckClassifier(roleBlock.Classifier2, umlBlock.Classifier2))
                {
                    // Both classifiers have local conformity 
                    rbmlBlocksAnswer.Add(umlBlock);

                    BlockLoggingService.Instance.LocalMatchSuccess(roleBlock, umlBlock);
                }
                else
                {
                    LoggingService.Instance.WriteDebug("RBML {0} failed local conformity with UML {1}", roleBlock, umlBlock);
                }

                BlockLoggingService.Instance.LocalConformityEnding(roleBlock, umlBlock);

            }
            return rbmlBlocksAnswer;
        }

        private List<ClassifierBlock> GetConformingAssociationBlocks(ClassifierBlock roleBlock, IEnumerable<ClassifierBlock> umlBlocks)
        {
            var rbmlBlocksAnswer = new List<ClassifierBlock>();
            foreach (var umlBlock in umlBlocks)
            {
                BlockLoggingService.Instance.LocalConformityStarting(roleBlock, umlBlock);
                // Check the different combinations of blocks 
                if ((CheckClassifier(roleBlock.Classifier1, umlBlock.Classifier1) && CheckClassifier(roleBlock.Classifier2, umlBlock.Classifier2)))
                {
                    // Both classifiers have local conformity 
                    rbmlBlocksAnswer.Add(umlBlock);
                    BlockLoggingService.Instance.LocalMatchSuccess(roleBlock, umlBlock);
                }
                else
                {
                    LoggingService.Instance.WriteDebug("RBML {0} failed local conformity with UML {1}", roleBlock, umlBlock);
                }

                BlockLoggingService.Instance.LocalConformityEnding(roleBlock, umlBlock);
            }
            return rbmlBlocksAnswer;
        }

        /// <summary>
        /// This should line up the uml/rbml classifiers.
        /// </summary>
        /// <param name="rbmlBLockLink"></param>
        /// <param name="umlBlock"></param>
        /// <returns>true if the 2 were lined up, false otherwise</returns>
        private static bool LineUpClassifiersForBlocks(BlockLink rbmlBLockLink, ClassifierBlock umlBlock)
        {
            var end1Match = Utilities.BlockTypeConverter.GetMatchingUmlBlockEnd(rbmlBLockLink.End1);
            var end2Match = Utilities.BlockTypeConverter.GetMatchingUmlBlockEnd(rbmlBLockLink.End2);


            if (umlBlock.Relationship.End1 == end1Match && umlBlock.Relationship.End2 == end2Match)
                return true;

            if (umlBlock.Relationship.End1 == end2Match && umlBlock.Relationship.End2 == end1Match)
            {
                SwapClassifiers(umlBlock);
                LoggingService.Instance.WriteDebug("Switched classifiers for to {0} for the UML block.", umlBlock);
                return true;
            }

            return false;
        }

        private static void SwapClassifiers(ClassifierBlock umlBlock)
        {
            var oldClassifier1 = umlBlock.Classifier1;
            var oldEnd1 = umlBlock.Relationship.End1;
            umlBlock.Classifier1 = umlBlock.Classifier2;
            umlBlock.Relationship.End1 = umlBlock.Relationship.End2;
            umlBlock.Classifier2 = oldClassifier1;
            umlBlock.Relationship.End2 = oldEnd1;
        }

        public bool CheckAttributeRoles(IEnumerable<ClassifierAttribute> rbmlAttributes, IEnumerable<ClassifierAttribute> umlAttributes, Tuple<string, string> assumedRbmlUmlMapping = null)
        {
            var candidates = rbmlAttributes.ToDictionary<ClassifierAttribute, IMultiplicity, IList<IMultiplicity>>(rbmlAttribute => rbmlAttribute, rbmlAttribute => GetAttributeCandidates(rbmlAttribute, umlAttributes));
            return CheckForMultiplicityMapping(candidates);
        }

        private List<IMultiplicity> GetAttributeCandidates(ClassifierAttribute rbmlAttribute, IEnumerable<ClassifierAttribute> umlAttributes, Tuple<string, string> assumedRbmlUmlMapping = null)
        {
            return new List<IMultiplicity>(umlAttributes.Where(umlAttribute => CheckAttribute(rbmlAttribute, umlAttribute)));
        }

        private bool CheckAttribute(ClassifierAttribute rbmlAttribute, ClassifierAttribute umlAttribute)
        {
            if (rbmlAttribute.Modifier != umlAttribute.Modifier)
                return false;

            if (rbmlAttribute.AttributeType.StartsWith("|"))
            {
                AddMappingToCheck(rbmlAttribute.AttributeType, umlAttribute.AttributeType);
                return true;
            }

            return rbmlAttribute.AttributeType == umlAttribute.AttributeType;
        }

        private static bool CheckForMultiplicityMapping(Dictionary<IMultiplicity, IList<IMultiplicity>> candidates)
        {
            //For all RBML Attributes
            foreach (var candidateKey in candidates.Keys)
            {
                var umlCandidates = candidates[candidateKey];

                //We could do a mapping, but it's a 0 so we don't need to
                if ((candidateKey.Multiplicity & Multiplicity.Zero) == Multiplicity.Zero)
                {
                    LoggingService.Instance.WriteDebug("{0} was skipped in mapping because it's multiplicity was {1}", candidateKey.ToString(), candidateKey.Multiplicity);
                    continue;
                }

                //This is the error state, we've somehow removed the candidates from this list.
                if (umlCandidates.Count == 0)
                {
                    LoggingService.Instance.WriteDebug("{0} could not be mapped to anything in the UML", candidateKey.ToString());
                    return false;
                }

                var umlItem = umlCandidates[0];
                LoggingService.Instance.WriteDebug("{0} was mapped to {1}", candidateKey.ToString(), umlCandidates[0]);

                foreach (var candidate in candidates)
                {
                    candidate.Value.Remove(umlItem);
                }
            }

            //note we've just checked that we met the bare minimum, not actually produced the mappings, though it could be done relatively easily.
            return true;
        }

        public bool CheckBehaviorRoles(IEnumerable<ClassifierBehavior> rbmlBehaviors, IEnumerable<ClassifierBehavior> umlBehaviors)
        {
            var candidates = rbmlBehaviors.ToDictionary<ClassifierBehavior, IMultiplicity, IList<IMultiplicity>>(rbmlBehavior => rbmlBehavior, rbmlBehavior => GetBehaviorCandidates(rbmlBehavior, umlBehaviors));
            return CheckForMultiplicityMapping(candidates);
        }

        private IList<IMultiplicity> GetBehaviorCandidates(ClassifierBehavior rbmlBehavior, IEnumerable<ClassifierBehavior> umlBehaviors)
        {
            return new List<IMultiplicity>(umlBehaviors.Where(behavior => CheckBehavior(rbmlBehavior, behavior)));
        }

        private bool CheckBehavior(ClassifierBehavior rbmlBehavior, ClassifierBehavior umlBehavior)
        {
            if (!CheckParameters(rbmlBehavior.Paramaters, umlBehavior.Paramaters))
                return false;

            if (rbmlBehavior.ReturnType.StartsWith("|"))
            {
                AddMappingToCheck(rbmlBehavior.ReturnType, umlBehavior.ReturnType);
                return true;
            }

            return rbmlBehavior.ReturnType == umlBehavior.ReturnType;
        }

        private bool CheckParameters(IList<Tuple<string, string>> rbmlParameters, IList<Tuple<string, string>> umlParameters)
        {
            if (rbmlParameters.Count != umlParameters.Count)
                return false;
            var potentialMaps = new List<Tuple<string, string>>();
            for (int i = 0; i < rbmlParameters.Count; i++)
            {
                var rbmlParamType = rbmlParameters[i].Item2;
                var umlParamType = umlParameters[i].Item2;

                if (rbmlParamType.StartsWith("|"))
                {
                    potentialMaps.Add(new Tuple<string, string>(rbmlParamType, umlParamType));
                    continue;
                }

                if (rbmlParamType != umlParamType)
                {
                    return false;
                }
            }

            _mappingsToCheck.AddRange(potentialMaps);
            return true;
        }

        // Check whether two classifiers conform to one another 
        private bool CheckClassifier(Classifier rbmlClassifier, Classifier umlClassifier)
        {
            //check if concreteness lines up, if the attributes line up and if the behaviors line up
            return rbmlClassifier.IsConcrete == umlClassifier.IsConcrete &&
                CheckAttributeRoles(rbmlClassifier.AttributeRoles, umlClassifier.AttributeRoles) &&
                CheckBehaviorRoles(rbmlClassifier.BehaviorRoles, umlClassifier.BehaviorRoles);
        }


        private static bool CheckGlobalMultiplicity(ClassifierBlock rbmlBlock, int numberOfUmlBlocks)
        {
            var minMult = GetMinimumSharedMultiplicityForBlock(rbmlBlock);
            var maxMult = GetMaximumSharedMultiplicityForBlock(rbmlBlock);

            if (minMult == Multiplicity.Zero)
            {
                if (maxMult == Multiplicity.Zero)
                    return numberOfUmlBlocks == 0;
                if (maxMult == Multiplicity.One)
                    return numberOfUmlBlocks <= 1;

                return numberOfUmlBlocks >= 0;

            }

            if (minMult == Multiplicity.One)
            {
                if (maxMult == Multiplicity.One)
                    return numberOfUmlBlocks == 1;

                return numberOfUmlBlocks >= 1;
            }

            if (minMult == Multiplicity.Many)
                return numberOfUmlBlocks > 1;

            return false;
        }

        private static Multiplicity GetMinimumSharedMultiplicityForBlock(ClassifierBlock block)
        {
            var classifierOneMult = GetMinimumMultiplicity(block.Classifier1);
            var classifierTwoMult = GetMinimumMultiplicity(block.Classifier2);

            return classifierOneMult > classifierTwoMult ? classifierOneMult : classifierTwoMult;
        }

        private static Multiplicity GetMaximumSharedMultiplicityForBlock(ClassifierBlock block)
        {
            var classifierOneMult = GetMaximumMultiplicity(block.Classifier1);
            var classifierTwoMult = GetMaximumMultiplicity(block.Classifier2);

            return classifierOneMult < classifierTwoMult ? classifierOneMult : classifierTwoMult;
        }

        private static Multiplicity GetMaximumMultiplicity(IMultiplicity multiplicity)
        {
            if (multiplicity.Multiplicity == Multiplicity.None)
                throw new InvalidEnumArgumentException("The multiplicity must be set.");

            if ((multiplicity.Multiplicity & Multiplicity.Many) == Multiplicity.Many)
                return Multiplicity.Many;

            return (multiplicity.Multiplicity & Multiplicity.One) == Multiplicity.One ? Multiplicity.One : Multiplicity.Zero;
        }

        private static Multiplicity GetMinimumMultiplicity(IMultiplicity multiplicity)
        {
            if (multiplicity.Multiplicity == Multiplicity.None)
                throw new InvalidEnumArgumentException("The multiplicity must be set.");

            if ((multiplicity.Multiplicity & Multiplicity.Zero) == Multiplicity.Zero)
                return Multiplicity.Zero;

            return (multiplicity.Multiplicity & Multiplicity.One) == Multiplicity.One ? Multiplicity.One : Multiplicity.Many;
        }
    }
}
